package com.partner.boot.service.impl;

import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.partner.boot.common.Constants;
import com.partner.boot.common.enums.EmailCodeEnum;
import com.partner.boot.controller.domain.LoginDTO;
import com.partner.boot.controller.domain.ResetPassRequest;
import com.partner.boot.controller.domain.UserRequest;
import com.partner.boot.entity.Permission;
import com.partner.boot.entity.Role;
import com.partner.boot.entity.RolePermission;
import com.partner.boot.entity.User;
import com.partner.boot.exception.ServiceException;
import com.partner.boot.mapper.PermissionMapper;
import com.partner.boot.mapper.RoleMapper;
import com.partner.boot.mapper.RolePermissionMapper;
import com.partner.boot.mapper.UserMapper;
import com.partner.boot.service.IUserService;
import com.partner.boot.utils.EmailUtils;
import com.partner.boot.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 菜牙买菜
 * @since 2022-12-07
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    private static final long TIME_IN_MS5 = 5 * 60 * 1000;  // 表示5分钟的毫秒数

    private static final String DEFAULT_PASSWORD = "123";

    @Autowired
    EmailUtils emailUtils;

    @Autowired
    UserMapper userMapper;

    @Resource
    RoleMapper roleMapper;
    
    @Resource
    PermissionMapper permissionMapper;

    @Resource
    RolePermissionMapper rolePermissionMapper;




    /**
     * 用户登录
     * @param user
     * @return
     */
    @Override
    public LoginDTO login(UserRequest user) {
        User dbUser;
        try {
            dbUser = getOne(new QueryWrapper<User>().eq("username", user.getUsername())
                    .or().eq("email",user.getUsername()));
        } catch (Exception e) {
            throw new RuntimeException("数据库异常");
        }
        if (dbUser == null) {
            throw new ServiceException("未找到用户");
        }
        // BCrypt.checkpw：将传入的密码与数据库查询出来的用户密码进行解密对比
        if (!BCrypt.checkpw(user.getPassword(),dbUser.getPassword())) {
            throw new ServiceException("用户名或密码错误");
        }
        StpUtil.login(dbUser.getUid());
        StpUtil.getSession().set(Constants.LOGIN_USER_KEY, dbUser); // 将用户信息存到redis
        String tokenValue = StpUtil.getTokenInfo().getTokenValue(); // 获取token值
        String roleFlag = dbUser.getRoleFlag();
        List<Permission> all = getPermissions(roleFlag);
        List<Permission> menus = getTreePermissions(all);
        List<Permission> auths = all.stream().filter(permission -> permission.getType() == 3).collect(Collectors.toList());
        return LoginDTO.builder().user(dbUser).token(tokenValue).menus(menus).auths(auths).build();
    }
    
    // 根据角色唯一标识查询出对应的权限菜单（水平菜单）
    public List<Permission> getPermissions(String roleFlag) {
        Role flag = roleMapper.selectOne(new QueryWrapper<Role>().eq("flag", roleFlag)); // 根据用户唯一标查询数据
        List<RolePermission> rolePermission = rolePermissionMapper.selectList(new QueryWrapper<RolePermission>()
                .eq("role_id", flag.getId())); // 根据查询出的角色的id获取对应的权限菜单集合
        List<Integer> permissionIds = rolePermission.stream()
                .map(RolePermission::getPermissionId).collect(Collectors.toList()); // 处理出权限菜单集合里的id
        List<Permission> permissionList = permissionMapper.selectList(null); // 获取所有权限菜单数据
        ArrayList<Permission> all = new ArrayList<>(); // 返回的水平菜单集合
        for (Integer permissionId : permissionIds) {
            permissionList.stream().filter(permission -> permission.getId().equals(permissionId)).findFirst().ifPresent(all::add);
        }
        return all;
    }

    /**
     * 根据传入的id查询用户数据，然后对比传入的密码是否与当前查出的用户密码一致
     * @param id 用户id
     * @param password 传入的就密码
     * @return 返回当前用户输入的密码是否与数据库里当前用户的密码 比较的结果
     */
    @Override
    public boolean checkPass(Integer id, String password) {
        User user = userMapper.selectById(id);

        return BCrypt.checkpw(password, user.getPassword()); // 判断密码是否正确，第一个参数是传入的密码，第二个参数是数据库加盐后的用户密码
    }

    /**
     * 用户修改密码
     * @param id 传入的id
     * @param password 需要修改的新密码
     * @return 返回修改的结果
     */
    @Override
    public boolean savePass(Integer id, String password) {
        User user = userMapper.selectById(id);
        user.setPassword(BCrypt.hashpw(password));
        int num = userMapper.updateById(user); // 返回修改的记录数量
        if (num >= 1) return true;
        return false;
    }


    private List<Permission> getTreePermissions(List<Permission> all) {
        // 菜单树 1级 -> 2级
        List<Permission> parentList = all.stream().filter(permission -> permission.getType() == 1
                        || (permission.getType() == 2 && permission.getPid() == null))
                .collect(Collectors.toList()); // pid = null 就是菜单
        for (Permission permission : parentList) {
            Integer pid = permission.getId();
            List<Permission> level2List = all.stream().filter(permission1 -> pid.equals(permission1.getPid())).collect(Collectors.toList());
            permission.setChildren(level2List);
        }
        return parentList.stream().sorted(Comparator.comparing(Permission::getSort).reversed()).collect(Collectors.toList());
    }

    // 发送邮件
    @Override
    public void sendEmail(String email, String type) {
        String emailPrefix = EmailCodeEnum.getValue(type);// 根据type获取邮箱枚举value值
        if (StrUtil.isBlank(emailPrefix)) {
            throw new ServiceException("不支持的邮箱验证类型");
        }

        // 设置redis的key
        String emailKey = Constants.EMAIL_CODE + emailPrefix + email;
        Long expireTime = RedisUtils.getExpireTime(emailKey);
        // 如果该验证码的时间还有，并且时间超过4分钟，则限制接口不发送邮件，并报出错误
        if (expireTime != null && expireTime > 4 * 60) {
            throw new ServiceException("邮件发送过于频繁");
        }
        // 判断邮箱是否注册
        User user = getOne(new QueryWrapper<User>().eq("email", email));
        if (EmailCodeEnum.REGISTER.equals(EmailCodeEnum.getEnum(type))) {
            if (user != null) {
                throw new ServiceException("该邮箱已注册");
            }
        } else if (EmailCodeEnum.RESET_PASSWORD.equals(EmailCodeEnum.getEnum(type))) {
            if (user == null) {
                throw new ServiceException("用户未找到");
            }
        }

        Integer code = Integer.valueOf(RandomUtil.randomNumbers(6)); // 随机生成6位数字验证码
        log.info("本次验证的code是：{}", code);
        String context = "<b>尊敬的用户：</b><br><br><br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;您好，" +
                "Partner交友网提醒您本次的验证码是：<b>{}</b>，" +
                "有效期5分钟。<br><br><br><b>Partner交友网</b>";
        String html = StrUtil.format(context, code);

        emailUtils.sendHtml("【Partner交友网】验证提醒",html,email);
        ThreadUtil.execAsync(() -> { // 异步请求
            // 设置redis缓存，key：email.code：register：邮箱
            String key = Constants.EMAIL_CODE + emailPrefix + email;
            RedisUtils.setCacheObject(key, code, TIME_IN_MS5, TimeUnit.MILLISECONDS);
        });

    }

    /**
     * 用户注册
     * @param user
     * @return
     */
    @Override
    public User register(UserRequest user) {
        String key = Constants.EMAIL_CODE + EmailCodeEnum.REGISTER.getValue() + user.getEmail();
        visEmailCode(key,user.getEmailCode()); // 校验邮箱验证码
        try {
            User saveUser = new User();
            BeanUtils.copyProperties(user,saveUser); // 将user里的属性值copy到User实体类中
            return saveUser(saveUser); // 插入数据
        } catch (Exception e) {
            throw new ServiceException("注册失败", e);
        }
    }


    /**
     * 重置密码前的用户邮箱验证
     * @param email
     * @param emailCode
     */
    @Override
    public void visResetPassEmailCode(String email, String emailCode) {
        String key = Constants.EMAIL_CODE + EmailCodeEnum.RESET_PASSWORD.getValue() + email;
        visEmailCode(key, emailCode);
    }

    /**
     * 重置用户密码
     * @param resetPassRequest
     */
    @Override
    public void resetPass(ResetPassRequest resetPassRequest) {
        User user = getOne(new QueryWrapper<User>().eq("email", resetPassRequest.getEmail()));
        if (user == null) { // 查询用户是否存在
            throw new RuntimeException("数据库查询失败");
        }
        resetPassRequest.setNewPass(BCrypt.hashpw(resetPassRequest.getNewPass()));
        int count = userMapper.updatePasswordByEmail(resetPassRequest);
        if (count <= 0) {
            throw new ServiceException("密码重置失败");
        }
    }

    /**
     * 获取用户信息
     * @return
     */
    @Override
    public User getUser() {
        return StpUtil.getSession().getModel(Constants.LOGIN_USER_KEY, User.class);
    }

    @Override
    public List<User> findAll() {
        List<User> users = userMapper.selectList(null);
        for (User user : users) {
            Role role = roleMapper.selectOne(new QueryWrapper<Role>().eq("flag",user.getRoleFlag()));
            user.setRole(role);
        }
        return users;
    }

    @Override
    public LoginDTO findOne(Integer id) {
        User user = userMapper.selectById(id);
        String roleFlag = user.getRoleFlag();
        List<Permission> all = getPermissions(roleFlag);
        List<Permission> menus = getTreePermissions(all);
        List<Permission> auths = all.stream().filter(permission -> permission.getType() == 3).collect(Collectors.toList());
        return LoginDTO.builder().user(user).token(null).menus(menus).auths(auths).build();
    }

    // 统一封装的验证邮箱类
    public void visEmailCode(String emailKey,String emailCode) {
        Integer code = RedisUtils.getCacheObject(emailKey);
        if (code == null) { // 如果获取的code为空，说明验证码已经失效
            throw new ServiceException("验证码已失效");
        }

        if (!emailCode.equals(code.toString())) { // 如果前端传入的email验证码与系统获取的邮箱验证码不一致，说明前端传入的邮箱验证码为错
            throw new ServiceException("验证码错误");
        }
        RedisUtils.deleteObject(emailKey); // 删除redis缓存
    }

    // 添加用户
    public User saveUser(User user) {
        User dbUser = getOne(new QueryWrapper<User>().eq("username", user.getUsername()));

        if (dbUser != null) { // 如果根据用户名在数据查到数据，证明该用户名重复
            throw new ServiceException("用户名重复");
        }

        if (StrUtil.isBlank(user.getName())) { // 如果前台未传入用户昵称，则设置默认昵称
            user.setName(getRandomName(5));
        }

        if (StrUtil.isBlank(user.getPassword())) { // 如果前台未传入用户密码，则设置默认密码
            user.setPassword(DEFAULT_PASSWORD);
        }

        Role defaultRole = roleMapper.selectOne(new QueryWrapper<Role>().eq("is_default", true));

        try {
            // 随机生成uuid，设置为用户的唯一标识
            user.setUid(IdUtil.fastSimpleUUID());
            user.setPassword(BCrypt.hashpw(user.getPassword()));
            user.setRoleFlag(defaultRole.getFlag()); // 默认普通用户角色
            save(user);
        }catch (Exception e) {
            throw new ServiceException("添加失败");
        }
        return user;
    }

    // 随机生成名字
    public String getRandomName(int len) {
        StringBuilder ret = new StringBuilder();
        for (int i = 0; i < len; i++) {
            String str = null;
            int hightPos, lowPos;
            Random random = new Random();
            hightPos = (176 + Math.abs(random.nextInt(39))); // 获取最高为
            lowPos = (161 + Math.abs(random.nextInt(93))); // 获取最低位
            byte[] bytes = new byte[2];
            bytes[0] = (new Integer(hightPos).byteValue());
            bytes[1] = (new Integer(lowPos).byteValue());

            try {
                str = new String(bytes, "GBK"); // 转成中文
            } catch (Exception e) {
                log.error("随机生成名字失败", e);
            }
            ret.append(str);
        }
        return ret.toString();
    }
}
